此文档处于 beta 阶段,非正式发布版本。
“本文环境”指CentOS Linux release 8.4.2105(内核版本5.10.25)环境。文中所述“默认值”均指单一主机上运行此环境时的默认值,仅用于对相关数值有大致概念。
指令
netstat
一般用于查看网络的统计信息,即执行 netstat -s 。
ss(Socket Statistics)
ss用于查看系统的 Socket 信息,较 netstat 更高效、强大。因为 netstat 是遍历 /proc 目录下的所有PID,而 ss 则利用了包括TCP协议栈中的分析统计模块等多种机制。
若我们想要查看所有正在监听中的 TCP 信息,可执行 ss -tl ,其可能的输出如下:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 511 0.0.0.0:http 0.0.0.0:* [::]:*
结果显示,我们开启了一个http服务器,输出中Recv-Q、Send-Q在不同的连接状态下分别代表:
|连接状态|Recv-Q|Send-Q| |:----|:----|:----| |LISTEN|当前全连接队列大小。|最大全连接队列大小。| |ESTABLISHED|未被进程读取的字节数|已发送但未被确认的字节数|
其他常用参数包括:
TYPICAL OPTIONS
-n, --numeric don't resolve service names
-r, --resolve resolve host names
-a, --all display all sockets
-l, --listening display listening sockets
-p, --processes show process using socket
-s, --summary show socket usage summary
-t, --tcp display only TCP sockets
USAGE EXAMPLES:
ss -o state established '( dport = :ssh or sport = :ssh )'
Display all established ssh connections.
ss -o state fin-wait-1 '( sport = :http or sport = :https )' dst 193.233.7/24
List all the tcp sockets in state FIN-WAIT-1 for our apache to network 193.233.7/24 and look at their timers.
内核配置
- 文件地址:/etc/sysctl.conf
- 查看配置:
sysctl -a - 临时设置:
sysctl -w net.core.somaxconn=32768 && sysctl -p或直接修改 /proc/sys/net/core/somaxconn 文件
TCP简介
TCP头部
TCP Header示意图如下:
传输数据紧跟在TCP Header之后,二者共同组成一个传输单位。
Sequence Number
包的序列号,用于保证数据的有序性,也即不乱序。
整个TCP周期,双方均以自行生成的 ISN(Initial Sequence Number) 作为初始Seq,将Seq作为每个报文的唯一标识。一方收到报文后,会发送一个 Acknowledgment Number(ack, Ack) = Sequence Number + 1 的回包给另一方确认报文已被收到。
Seq的递增规律不是稳定在每次加1的,而是与上一序列对应的包的数据大小有关。
在使用Wireshark抓取报文时,Seq值可能从0开始,其仅仅是为了展示友好,使用了相对序列号。点击报文详情或修改相关设置可查看真实Seq。 ISN不会从0开始,而是动态算出的。因为网络是不可靠的,如果每次连接都从0开始,很容易出现以下情况。
也即服务端收到的数据将由新老数据共同组成,出现严重的问题。但是,动态 ISN无法完全解决此问题,只能减少发生的概率。
同时,动态 ISN 可以预防TCP序列预测攻击。
TCP的ISN计算公式为:
ISN = M + F (localhost, localport, remotehost, remoteport)
其中:
M为计时器,每隔4微秒加1。
F为Hash算法。
当M超过2^32后,会再次从0开始,也即一个周期为4 * 2^32毫秒 ≈ 4.55小时。所以只要MSL不超过此值,就不会存在重用 ISN 的情况。
Acknowledgement Number(ack, Ack)
下一个待接收包的Seq号,用于保证包的连续性,也即不丢包。
Flags(状态标识)
包的类型标识,用于操作TCP的状态机。含SYN(synchronous 建立连接)、ACK(acknowledgement 确认)、PSH(push 传送)、FIN(finish 结束)、RST(reset 重置)、URG(urgent 紧急)。
注意,包括ACK在内的状态位仅占1bit,也即其只会是0或1。状态标识ACK与Acknowledgement Number(ack, Ack)不是同一概念,需注意区分。在本文中,将以大小写进行区分。
Window(Advertised-Window)
滑动窗口(Sliding Window)机制中,接收方使用 Window 来告知发送方当前还可以接收多少数据,用于流量控制。
滑动窗口
TCP的双方都会维护两个队列,即发送队列、接收队列。在以下的示例中,整个数据队列即为处于内核的写、读缓存区。
发送队列
将需要发送的数据不断的加入到队列尾部。队列中存在两个指针和一个参数:
- 确认指针(Send Unacknowledged, SND.UNA):指向最早的已发送但未确认的数据。
- 发送指针(Send Next, SND.NXT):指向最早的的可发送但还未发送的数据。
-
发送窗口的长度(Send Window, SND.WND):当前可处于发送状态的数据大小。
当存在可用窗口时,发送指针后的将向右移动,其经过的数据将被发送。
假定我们现在接收到了代表偏移量18对应数据的ACK报文,接下来会:
- 移动确认指针使其指向18号报文。
- 根据该ACK报文中的 Window 信息,动态调整发送窗口长度。
-
根据
确认指针 + 发送窗口长度 - 发送指针可得出新的可用窗口大小。发送窗口的长度的长度可能变长也可能变短,所以不一定会产生新的可用窗口。 当用户程序需要发送数据时,会在 #4 区域写入数据,如果剩余缓存不足以容纳所有需要写入的数据,内核会通过返回值告知用户程序有数据未被写入。在很多语言中,此时会把数据暂存于用户内存中,适时再次进行内核调用写入数据。如果网络阻塞,数据会在用户内存中越积越多,会存在内存溢出的风险,需要相关机制进行保护。
如在 Node.js 中,可以使用pipe或者根据返回值暂停写入并等待drain事件。
接收队列
将接收的数据不断的加入到队列尾部,同时用户程序将从队列头部不断的读取 #1 区域的数据。队列中存在一个指针和一个参数:
- 接收指针(Receive Next, RCV.NXT):指向最久未被收到的数据。
-
接收窗口长度(Receive Window, RCV.WND):当前可以接收的数据大小,其将在回包中通过 Window 字段告知发送方,在完成同步后,对端的发送窗口长度会等于本端的接收窗口长度。
当处于上图状态时,接下来可能发生:
- 收到偏移量19对应的数据,偏移量19对应的报文将通过ACK回包被确认收到。
- 收到偏移量16对应的数据,接收指针指向的数据被收到,将发生:
- 移动接收指针使其指向偏移量19。
- 根据
接收指针 + 接受窗口长度可得出新的#2、#3区域边界,也即26、27、28号数据将进入接收窗口,被允许发送和接收。在极端情况下,因为用户一直未读取 #1 部分的数据,致使整个接收缓存区被全部使用,接受窗口无法再向右移动,此时将会进行收缩窗口,也即减小接收窗口长度。
- 用户读取 #1 部分的数据,对应数据将被删除,接收缓存区的可用大小变大,接收窗口将被增大。
Zero Window
极端情况下,未被用户读取的数据会使缓存溢出,使滑动窗口(接收窗口)的长度变成0,不会再进行任何的数据传输。若之后程序读取了数据,使滑动窗口长度不再为0,由于不再存在任何数据传输,无法将此信息通过 Window 字段同步给发送端。
TCP使用窗口探测(Zero Window Probe, ZWP)机制解决此问题,当滑动窗口长度变成0时,发送端将发送ZWP报文给接收端,以确保能知晓滑动窗口长度的变化。
Silly Window Syndrome(糊涂窗口综合征, SWS)
MSS(maximum segment size):TCP报文中的数据部分(即不含TCP头部)的最大长度。在三次握手中的 SYN 包中利用 Options 字段交换此信息。
![]()
当接收方处理数据缓慢造成**滑动窗口(接收窗口)**变得极短、或发送端产生数据很慢时,会存在频繁发送小数据的情况,致使数据大小甚至不如TCP头部大,造成资源浪费。
- 发送方优化:发送方可使用 Nagle 算法,即小数据不会被立即发送,而需等到某些事件发生。如累积的小数据达到一定大小、等待时间超时等。
TCP_NODELAY: 对于实时性要求比较高的场景,我们可以通过此选项来禁用 Nagle 算法。
- 接收方优化:当滑动窗口(接收窗口)过短时,直接将回包中的 Window 置为0。如滑动窗口长度小于MSS、滑动窗口长度小于缓存空间的一半等。
net.ipv4.tcp_window_scaling
TCP头部中,Window 字段为16位,其限制了滑动窗口最大为2 ^ 16 = 64KB。当我们需要支持更大的窗口时,则可开启此设置。其可在TCP header的Options字段中中指定窗口的扩大信息,最大可扩大 2 ^ 14 倍,也即窗口最大值将变为 2 ^ 16 * 2 ^ 14 = 1GB。此参数默认值为1。
重试机制
超时重传
当A方发送了Seq 1、2、3、4的报文后,若B方收到了Seq 1、2、4,其会回复Ack 3表示其还未收到3。在超时时间后,若A方依然只收到了Ack 3,则会进行重传。此时有两种方案:
- 仅重传Seq 3的包。
- 重传Seq 3及其后的所有包。 TCP会根据发送到收到ACK的时间间隔(Round Trip Time, RTT),动态设置超时时间(Retransmission Timeout, RTO)。若同一个序列号的包被重复触发重传,会使用加倍timeout机制。
快速重传
TCP引入了Fast Retransmit算法,在上面的示例中,当连续3次收到Ack 3时,将直接开始重传Seq 3而无需等到timeout。
SACK机制
SACK(Selective Acknowledgment),即在ACK应答中,添加一个额外的SACK信息,用于告知对方我已收到的数据序列范围。在进行重试时,对方就可以根据这个信息选择性的进行重传,而无需在仅重传下一个序列还是重传接下来的所有序列间做出选择。
相关参数
- net.ipv4.tcp_sack:是否开启SACK机制,默认值为1。
D-SACK
D-SACK(Duplicate SACK)用于告知发送端,哪些数据被我方重复接收了,可协助发送端了解网络情况,以实现更好的流量控制。其利用SACK传输信息,如SACK的第一段范围与ACK间有重合,则说明是D-SACK。
相关参数
- net.ipv4.tcp_dsack:是否开启D-SACK机制,默认值为1。
拥塞控制
如果TCP不进行拥塞控制,当网络繁忙时,所以发送端均一直进行超时重试,进一步加剧网络负担,最终使整个网络崩溃。
TCP的拥塞控制主要通过控制发送窗口的大小来间接控制,相关参数为:
- rwnd(Receive Window):接收窗口大小。
- cwnd(Congestion Window):拥塞窗口大小,如果出现诸如发生重传等表明网络发生阻塞的迹象时,该值会变小,反之会变大。
- swnd(Send Window):发送窗口大小,其大小将不仅受ACK回包中的 Window 控制,也受cwnd间接控制。 发送窗口的计算公式为:
swnd = min(cwnd, rwnd)
执行 ss -i 可以查看连接的内核信息,如cwnd、rcv_space、rcv_ssthresh等。
在相关机制的作用下,cwnd的值变化将呈下图所示趋势。我们将逐步分析其形成过程。
慢启动(Slow Start)
- 连接建立之后,cwnd初始值为1。
- 每收到一个ACK回包,cwnd加1。 依据以上逻辑,cwnd的大小会呈指数级增长。如cwnd = 1时,收到一个ACK,cwnd变为2。下一次便可以同时发送2个报文,并接收到2个ACK,cwnd变成了4。
拥塞避免(Congestion Avoidance)
为了缓和指数级增长曲线,引入了ssthresh(Slow Start Threshold,慢启动阈值) :
- 当cwnd >= ssthresh时,使用拥塞避免算法,即每收到一个ACK回包,cwnd 加 1/cwnd。 也即,需要收到cwnd个ACK回包,cwnd才会加1。最终的曾长曲线如下:
拥塞发生算法
已被废弃的做法(TCP Tahoe) 当出现快速重传时,我们便认为已经存在一定程度的网络拥塞。此时:
- ssthresh被设置为 cwnd / 2。
- cwnd将被重置为1。
- 重新开始开始慢启动过程。 这过于激进。 当出现快速重传时,其意味着我们还是收到了包,所以我们认为整个网络状态还是良好的。此时:
- cwnd 被设置为 cwnd / 2。
- ssthresh被设置为cwnd。
- 进入快速恢复状态。
快速恢复
- cwnd = ssthresh + 3。 // 那干嘛之前cwnd = cwnd / 2?
- 重传丢失的数据报。
- 收到重复ACK时,cwnd = cwnd + 1。
- 收到新数据的ACK时,cwnd = ssthresh。// TODO 把 cwnd 设置为第一步中的 ssthresh 的值
TCP与UDP
| |TCP|UDP| |:----|:----|:----| |连接|面向连接|无连接| |通信方式|一对一|一对一、一对多、多对多| |可靠性|可靠,包括有序、不丢失等|不可靠| |拥塞控制|有|无,网络拥堵也不影响其发送速率| |效率|相对较低|高| |适用范围|完整性,安全性要求较高|实时性要求较高,如直播、电话会议。|
三次握手
过程
- 客户端发起 SYN(客户端请求同步),同时该TCP进入 SYN_SNED状态。
- 服务端收到后回复 ACK(确认客户端的同步请求)和 SYNC(服务端请求同步),并将该TCP转换为 SYNC_RCVD状态。同时将此连接放入 Syns Queue (半连接队列)中。
- 客户端收到后回复 ACK(确认服务端的同步请求)。若服务端超时未收到此 ACK,则会重传第2步中的相关报文,重传次数受
net.ipv4.tcp_synack_retries控制。当收到此 ACK后,服务端将尝试把此连接从 Syns Queue(半连接队列)转移到 Accept Queue(全连接队列)。
报文预览
相关参数
net.ipv4.tcp_syn_retries
当第1次握手后,客户端超时未收到服务端发来的ACK报文(也即第2次握手)时进行重试的最大次数,默认值为6。
net.ipv4.tcp_synack_retries
当第2次握手后,服务端超时未收到客户端发来的ACK报文(也即第3次握手)时进行重试的最大次数,默认值为5。
BACKLOG
半连接队列(Sync Queue)
内核收到SYN请求后,会将该连接放入半连接队列并回复SYN+ ACK。半连接队列的大小无法直接查看到,可通过 ss 指令筛选出处于 SYN_RECV状态的连接以确定当前半连接队列大小。通过 netstat -s | grep "sockets dropped" ,我们可以查看半连接队列的溢出次数,其输出如下:
[root@6b69b89abdb2 /]# netstat -s | grep "sockets dropped"
146734 SYNs to LISTEN sockets dropped
net.ipv4.tcp_max_syn_backlog
此设置项可用于设置半连接队列大小。但是,内核在把一个 Socket 放入半连接队列时,还会依据全连接队列的大小相关的参数做一些判断,所以若想增加半连接队列大小,还需要一同增加全连接队列的大小。
全连接队列(Accept Queue)
该队列中存储了完成了三次握手,已处于 established 状态但尚未被应用调用 accept 函数取走的 Socket。
我们可以通过 netstat -s | grep "socket overflowed" 查看全连接队列的溢出次数,其输出如下:
[root@6b69b89abdb2 /]# netstat -s | grep "socket overflowed"
141960 times the listen queue of a socket overflowed
net.core.somaxconn
somaxconn的全称为socket max connections。
全连接队列大小 = min(backlog, net.core.somaxconn)
其中:
backlog为程序调用系统指令时传入的参数。
net.ipv4.tcp_abort_on_overflow
当服务端收到第3次握手并尝试将 Socket 从 Sync Queue 转入 Accept Queue 时,若发现Accept Queue已满,则根据此配置进行不同的处理:
- 0:直接丢弃该ACK,此Socket会在下次发送数据时再次被尝试放入Accept Queue ,此为默认值。
- 1:发送RST给客户端以断开连接。
示例
- 执行
nc -lk 10086开启一个TCP监听10086端口。 - 执行
ss -ltp | grep 10086查看此监听器。
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 1 0.0.0.0:10086 0.0.0.0:* users:(("nc",pid=27746,fd=3))
我们发现其backlog被设置成了1,同一时间仅会有一个established连接处于未被接收状态。
3. 执行 nc 0.0.0.0 10086 连接此TCP监听器。
4. 执行 ss sport = 10086 查看与10086端口的连接信息。以下信息表明,端口47764与10086建立了连接。
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp ESTAB 0 0 127.0.0.1:47764 127.0.0.1:10086
- 执行
nc 0.0.0.0 10086连接10086。 - 再次执行
ss sport = 10086查看与10086端口的连接信息。
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp ESTAB 0 0 127.0.0.1:47764 127.0.0.1:10086
tcp ESTAB 0 0 127.0.0.1:57278 127.0.0.1:10086
- 执行
ss -l sport = 10086查看此监听器。
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 1 1 0.0.0.0:10086 0.0.0.0:* users:(("nc",pid=31383,fd=3))
我们发现Recv-Q变成了1,即有一个 established 连接未交付给应用。
三次握手的必要性
-
避免新老 SYN 同时存在引发混乱。具体情况如下:
若只有两次握手,客户端将不会对历史请求回复RST,服务端和客户端将处于完全不一致的状态。
- 同步 ISN,因为 ISN 不是从0开始,在建立连接过程中需要告知对方。若不交换 ISN 而直接开始传输数据,将无法确认哪个数据是第一条数据。
- 确保双方的收发能力正常,完全有可能中间某个路由器设置了防火墙,使用了准出不准入等策略。双方都需要知晓自己与对方的发送、接收能力正常。
- 第一次握手后,服务端收到请求,服务端确认了客户端发送能力、服务端的接收能力正常。
- 第二次握手后,客户端收到请求,客户端确认了服务端的发送、接收能力正常,客户端发送、接收能力正常。
- 第三次握手后,服务端收到请求,此时服务端才能确定服务端的发送能力、客户端的接收能力正常。
四次挥手
过程
连接断开的发起方可能是客户端,也可能是服务端。在此,我们以A端表示主动断开方,以B端表示被动断开方。
- A端发起 FIN以发起断开过程,同时A端进入 FIN-WAIT-1状态。
- B端收到后回复 ACK以确认收到A端的断开请求,同时进入 CLOSE-WAIT状态,同时A端收到后转入 FIN-WAIT-2状态。
- 一段时候后发送 FIN, ACK,并将该TCP转换为 LAST_ACK状态。
- A收到后回复 ACK。若B端超时未收到此 ACK,则会重传第3步中的报文。
报文预览
实验中发现存在合并第2、3次挥手的现象,如下。遗憾的是未能重现,无法验证什么情况下才会出现合并的情况。
相关参数
net.ipv4.tcp_orphan_retries
孤儿连接:不在有Socket描述符与之绑定但依然存在于内存中的socket。此时在网络连接中可以查看到此端口,但无法通过 lsof 等指令查看到对应的PID。 此参数不仅对孤儿连接有效,也可用于限制发送 FIN 包(第3次挥手)的超时重试次数,默认值为0,特指8次。
net.ipv4.tcp_fin_timeout
Socket等待最终 FIN 包的超时时间,也即主动断开方处于 FIN_WAIT-2 的超时时间,若超时还未收到被动断开方发来的 FIN 包则直接断开连接,默认值为60。
net.ipv4.tcp_max_orphans
内核接管的孤儿连接的最大数量,当溢出时,新的孤儿连接将直接发送 RST 报文强制关闭。
TIME-WAIT 状态
存在的原因
- 确保被动端被正确的关闭。 因网络传输是不稳定的,若第4次挥手未被被动端接收,则被动端会重发第3次挥手,且重发会在2 * MSL时间内被A收到。所以A方需等到2MSL,以确保B端被正确的关闭。
若B端被困在LAST-ACK状态,当A端再次使用此端口向A端发起三次挥手将会得到RST回复。
- 防止旧连接的数据包 确保现有的数据报文都失效,以免使用同一端口建立了新连接后,旧的数据报文被当作新连接的数据报文。注意我们保证不冲突的是ISN,而Seq是有可能重复的。
引发的问题
MSL(Maximum Segment Lifetime):报文最大生存时间,当报文存活时间超过此值后,将被视为无效,被整个网络抛弃。 TIME-WAIT状态下,连接依然存在,也即其依然占用着端口、句柄、内存等资源,且其会持续2MSL(在本文环境下默认为2*30=60秒)时间,在高并发场景下影响较大。
优化
net.ipv4.tcp_max_tw_buckets
处于TIME-WAIT状态的最大连接数,无法被容纳的连接将不进入TIME-WAIT状态而直接被清理,正如TIME-WAIT存在的原因中所述,这是存在风险的。在本文环境下其默认值为8192。
net.ipv4.tcp_timestamps
⚠️ 不要在无技术专家指导的情况下修改此参数。 在TCP Header中添加了2个4字节的时间戳字段,分别存储发送方和接收方的时间戳。其是以下优化方式的前提条件,需通信双方均开启时才有效。
net.ipv4.tcp_tw_reuse
⚠️ 不要在无技术专家指导的情况下修改此参数。 开启后,将允许复用处于TIME-WAIT状态超过1秒的Socket用于发起新的连接请求。面对以上两个问题时:
-
B端处于LAST-ACK时,A端发送SYN包,B端发现SYN包中时间戳的更新,则不会回应RST,而是FIN,ACK,A端此时处于SYN-SENT状态,收到FIN,ACK时,会回复RST,最终B端收到RST并将旧连接关闭。之后A端再次发送SYN报文,即可正常进行三次握手。
-
对于旧连接的数据报文,因为时间戳时间过旧,将会直接被抛弃。
net.ipv4.tcp_tw_recycle
⚠️ 不要在无技术专家指导的情况下修改此参数。 ⚠️ 此参数已于4.12内核开始弃用 如果主动断开方是服务器,系统会记录每个IP+端口的分组时间戳,当新的SYN请求来时,若其时间戳比之前记录的同一IP+端口的连接记录新,则会复用此处于TIME-WAIT的连接,否则抛弃。或者说,服务器会丢弃掉同一个IP+PORT的所有时间戳小于上次记录的时间戳的数据包。
但当报文经过NAT转发时,其时间戳由真正的客户端生成,但多个请求却复用了NAT的IP+端口,在服务器看来,同一个连接后发的包时间戳并不一定更大,容易造成误杀,所以不建议NAT环境下使用此功能。
自4.10内核开始,时间戳生成机制被修改,此参数在任何环境下都不建议使用。
为什么不是3次挥手
因为主动端发起断开后,被动端可能还有数据需要处理和发送,所以会先返回ACK应答,待自己处理完毕后,再发送FIN报文开始断开。而三次握手中,第二次握手将SYN和ACK合并发送了,节省了一次通信。
半连接案例
步骤
- 服务端执行 sudo iptables --append INPUT --protocol tcp --dport 10086 --tcp-flags ACK ACK --jump DROP 丢弃掉发送到10086的 ACK 报文。
- 服务端开启TCP监听,ss -at | grep 10086 结果如下:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 3 *:10086 *:*
- 客户端尝试连接服务端的10086接口。
- 执行 ss -at | grep 10086 发现。有一个连接处于SYN-RECV状态。
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 3 *:10086 *:*
SYN-RECV 0 0 [::ffff:10.10.10.50]:10086 [::ffff:10.10.10.6]:53534
- 执行 sudo iptables -L INPUT --line-numbers 查看iptables限制。
- 执行 sudo iptables -D INPUT [num] 删除第1步添加的限制。
相关报文
理论连接数
端口限制
每个TCP连接会占用一个文件描述符,系统使用4元组作为TCP连接的唯一标识,即{local_ip, local_port, remote_ip, remote_port}。其中ip为4字节,port为2字节。
net.ipv4.ip_local_port_range
本地可用端口范围,默认值为32768 60999,分别代表最小端口和最大端口号。TCP/IP协议簇中的端口为u2类型,也即做多有2 ^ 16 = 65536个端口。0端口被用于告知操作系统在动态端口号范围内搜索接下来可以使用的端口号,所以可用端口最大可设置为65535个。
客户端限制
客户端每次发起TCP请求时,均会选取一个空闲端口作为通信端口,所以可用端口号将限制TCP连接数。注意,一个可用端口可以被复用,用于连接处于不同ip或端口的服务器。如下输出中,本地端口32768与远端服务器的10086、10087端口分别建立了连接。
[root@91bca93ccb20 /]# ss -atp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 172.17.0.2:32768 192.168.101.2:10086 users:(("nc",pid=261,fd=3))
ESTAB 0 0 172.17.0.2:32768 192.168.101.2:10087 users:(("nc",pid=262,fd=3))
服务端限制
服务端在一个端口开启监听并接受连接请求后,无需额外的端口。依据TCP连接的唯一标识,各连接的不同在于remote_ip(32位)、remote_port(16位),也即一个监听端口可保持的最大TCP连接数为2 ^ 32 * 2 ^ 16 = 2 ^ 48次方。当然这仅是理论,实际还受内存、最大文件描述符数量限制。每个Socket占用15~20KB内存,且每个Socket都是一个文件描述符。
缓存限制
net.core.wmem_max & net.core.rmem_max
Socket写、读缓存的最大值,单位bytes。默认值为1048576,即1MB。
net.core.wmem_default & net.core.rmem_default
Socket写、读缓存的默认值,单位bytes,会被 net.ipv4.tcp_wmem、net.ipv4.tcp_rmem 覆盖。默认值为124928,即122KB。
net.ipv4.tcp_wmem
TCP Socket Write Buffer的长度,三个参数分别为最小值、默认值、最大值,单位为字节。具体值会根据当前内存压力动态调节。默认值为4096 16384 4194304。
用户程序可以主动设置SO_SNDBUF,但这是不建议的,彼时将直接使用此值作为写缓存大小,也即会取消动态调节。若此值被设置得过小,有可能出现网络延迟较大的现象。因为滑动窗口是需要等到ACK之后才有可能继续发送接下来的数据的,但如果滑动窗口较小,则同时发送的数据也更少。
理论上,若想让一个连接充分利用带宽,此值应设置得比 BDP(Bandwidth Delay Product,带宽时延积)略高。
BDP = RTT * (带宽 / 8)
其中:
RTT为时延(Round Trip Time)。
若继续增大,则性能瓶颈已不再是缓存大小,而是带宽、时延等。
使用
ss -itm可以查看TCP连接的内存使用等信息。
net.ipv4.tcp_rmem
TCP Socket Recrive Buffer的长度。三个参数的意义同 net.ipv4.tcp_wmem 。默认值为4096 131072 6291456。
用户程序也可以通过SO_RCVBUF直接设置读缓存大小,同样也不建议进行设置。
其参数的理想值的计算逻辑与写缓存类似,但可高于写缓存以防接收方无法及时处理数据而出现收缩窗口的情况,可考虑直接翻倍。
net.ipv4.tcp_moderate_rcvbuf
是否开启接收缓存大小的动态调节,默认值为1。
net.ipv4.tcp_mem
其由A、B、C共3个数字组成,其单位为页。页的大小可通过 getconf PAGESIZE 查看,输出单位为bytes,在本文环境下为4KB。内核将根据TCP内存占用情况,以A、B、C作为阈值调节net.ipv4.tcp_rmem。
- 当小于A时,不进行干预。
- 当介于A、B之间时,内核进入 memory pressure 压力模式,并开始干预。
- 当大于C时,不再为TCP分配内存,新连接将无法建立。
net.ipv4.tcp_adv_win_scale
实际上,Read Buffer的长度是 SO_RCVBUF * 2,但会有部分空间被用于存储overhead数据,也即诸如TCP头、IP头等除数据本身(payload)之外的其他数据。 此参数将影响最终接收窗口的大小,其默认值为1。
最终接收窗口大小为:
- 当tcp_adv_win_scale > 0时:2 * SO_RCVBUF * 1 / (2 ^ tcp_adv_win_scale)。
- 当tcp_adv_win_scale <= 0时:2 * SO_RCVBUF * (1 - 1 / 2 ^(-tcp_adv_win_scale))。 如SO_RCVBUF为128KB,则实际有256KB,而接受窗口将占用其中的1 / (2 ^ 1) = 1 / 2,即128KB。具体计算函数逻辑为:
// space = 2*SO_RCVBUF。
static inline int tcp_win_from_space(const struct sock *sk, int space){
int tcp_adv_win_scale = sock_net(sk)->ipv4.sysctl_tcp_adv_win_scale;
return tcp_adv_win_scale <= 0 ?
(space>>(-tcp_adv_win_scale)) :
space - (space>>tcp_adv_win_scale);
}
描述符限制
程序级限制
ulimit
ulimit -n 将输出当前用户最多可以打开的文件数量。ulimit -n [value]可临时修改该限制,通过/etc/security/limits.conf配置文件可永久修改:
* soft nofile [value] # 进程运行的时候可修改软限制的值但不能超过硬限制。
* hard nofile [value] # 进程无法修改,除非以superuser身份运行。
ulimit -a用于查看用户能打开的最大进程数和文件句柄数。当我们cat /proc/[PID]/limits时,可发现其与limit -a的输出相符。ulimit命令可用于修改多种资源限制,如最大进程数量等。
fs.nr_open
单个进程可打开的最大文件数,默认值为1048576。
全局限制
fs.file-max
系统总共可打开的文件描述符数量,一般情况下不修改,默认值为524288。
我们可以通过 fs.file-nr 查看已分配句柄数、已分配但未被使用的句柄数、最大句柄数。如下:
[root@6b69b89abdb2 /]# sysctl -a | grep fs.file-nr
fs.file-nr = 768 0 524288
net.netfilter.nf_conntrack_max
iptables防火墙使用ip_conntrack内核模块实现**连接的跟踪。**在其开启的情况下,所有连接都会被跟踪,iptables也维护着一个跟踪表,当其满的时候,就会出现丢包的情况。此参数用于设置iptables跟踪表的最大容量。也可通过不使用ip_conntrack来避免丢包的问题。默认值为
65536。
攻击
SYN Flood Attack
不停的发送非法SYN请求,但不回复第三次握手,致使服务端的半连接队列被填满,无法响应正常的用户请求。这是一种常见的 DoS攻击(denial-of-service attack,拒绝服务攻击)。
减小 net.ipv4.tcp_synack_retries 和增大半连接队列(需同时增加全连接队列)可以在一定程度上缓解此攻击。
net.ipv4.tcp_syncookies
当半连接队列溢出时,TCP不会保持新的Socket,也就不存在将此Socket存入半连接队列。反之会根据SYN包信息计算一个cookie值,在收到第3次握手时,将检查ACK包中对应的cookie值,没问题则将此Socket放入全连接队列。
可选参数包括:
- 0:关闭该功能。
- 1:当SYN半连接队列满时启用,为默认值。
- 2:无条件启用。 注意,此机制是违反TCP协议的,会在客户端触发一些问题,仅作为解决SYN Flood攻击的最后防线,不可作为解决负载问题的方案。
其他参数
net.core.netdev_max_backlog
网络接口接收数据包的速度超过内核处理他们的速度时,在输入端排队的最大数据包数量,默认值为30000 。
net.ipv4.tcp_keepalive_time
表示TCP持续多少秒没有数据传输则启动探测报文(发送空的报文)以进行keep-alive probes,默认值为7200。
net.ipv4.tcp_keepalive_intvl
探测报文间的时间间隔,默认值为75。
net.ipv4.tcp_keepalive_probes
keep-alive probes的探测次数,默认值为9。
// TODO https://perthcharles.github.io/2015/09/07/wiki-tcp-retries/